// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/gateio_futures/api_order/native_private_client.h"

#include <memory>
#include <sstream>

#include "coin2/exchange/gateio/api_util/auth_key.h"
#include "coin2/exchange/gateio/api_util/rate_tracker.h"
#include "coin2/exchange/gateio_futures/symbology/product.h"

using coin2::exchange::gateio::api_util::GateioRateTracker;
using coin2::exchange::gateio_futures::symbology::GateioFuturesProduct;

GateioFuturesNativePrivateClient::GateioFuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config,
    bool is_delivery)
    : is_delivery_{is_delivery} {
  mea_ = mea;
  NativeClientConfig cfg;
  cfg.set_rest_host("api.gateio.ws");
  // TODO(hyuan): Further test is required.
  // cfg.set_rest_host("fx-api-testnet.gateio.ws");
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[GateioFuturesNativePrivateClient] config: " << cfg.DebugString();

  auths_.Construct<GateioRestAuth, NativeClientConfig>(cfg);

  if (is_delivery_) {
    path_query_account_balance_ = "/api/v4/delivery/{}/accounts";
    path_query_account_position_ = "/api/v4/delivery/{}/positions";
    path_query_open_orders_ = "/api/v4/delivery/{}/orders";
    path_query_hist_orders_ = "/api/v4/delivery/{}/orders";
    path_query_hist_trades_ = "/api/v4/delivery/{}/my_trades";
    path_place_order_ = "/api/v4/delivery/{}/orders";
    path_cancel_order_ = "/api/v4/delivery/{}/orders/{}";
  } else {
    path_query_account_balance_ = "/api/v4/futures/{}/accounts";
    path_query_account_position_ = "/api/v4/futures/{}/positions";
    path_query_open_orders_ = "/api/v4/futures/{}/orders";
    path_query_hist_orders_ = "/api/v4/futures/{}/orders";
    path_query_hist_trades_ = "/api/v4/futures/{}/my_trades";
    path_place_order_ = "/api/v4/futures/{}/orders";
    path_cancel_order_ = "/api/v4/futures/{}/orders/{}";
  }
  rate_tracker_ = std::make_unique<GateioRateTracker>(mea_, config);
  start_time_ = GetCurrentTimestamp();
}

auto GateioFuturesNativePrivateClient::QueryAccountBalance(const RestParams& params)
    -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryAccountBalance] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  std::string path = fmt::format(path_query_account_balance_, params.settle());
  auto context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::QueryAccountPosition(const RestParams& params)
    -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryAccountPosition] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  std::string path = fmt::format(path_query_account_position_, params.settle());
  auto context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::QueryOpenOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryOpenOrders] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("contract", params.contract());
  builder.PutGet("status", "open");
  std::string path = fmt::format(path_query_open_orders_, params.settle());
  auto context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::QueryHistOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryHistOrders] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("contract", params.contract());
  builder.PutGet("status", "finished");
  std::string path = fmt::format(path_query_hist_orders_, params.settle());
  auto context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::QueryHistTrades(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryHistTrades] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  std::string path = fmt::format(path_query_hist_trades_, params.settle());
  auto context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::QueryTradingFee() -> HttpContextPtr {
  if (is_user_id_ready_) {
    return {};
  }

  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryTradingFee] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_trading_fee_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  // context->query_type = GateioFuturesOrderSubTopicId::REST_TRADING_FEE;
  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

std::string GateioFuturesNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "GateioFuturesNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

void GateioFuturesNativePrivateClient::SetUserId(const std::string& channel, void* data) {
  std::string user_id = *(static_cast<std::string*>(data));
  if (!user_id.empty()) {
    is_user_id_ready_ = true;
    LOG(INFO) << "[SetUserId] user id ready: " << user_id;
  } else {
    LOG(INFO) << "[SetUserId] Empty user id: " << channel;
  }
}

void GateioFuturesNativePrivateClient::Prepare() {
  mb_->Subscribe(
      "gateio_futures/user_id",
      std::bind(
          &GateioFuturesNativePrivateClient::SetUserId,
          this,
          std::placeholders::_1,
          std::placeholders::_2));
}

auto GateioFuturesNativePrivateClient::PlaceOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "[PlaceOrder] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("contract", params.contract());
  builder.PutPost("size", params.size());
  builder.PutPost("price", params.price());
  builder.PutPost("tif", params.time_in_force());
  builder.PutPost("text", params.text());
  std::string path = fmt::format(path_place_order_, params.settle());
  auto context = GetAuth()->CreateRequestByPostWithSignature(path, &builder);
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::CancelOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "[CancelOrder] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  std::string path = fmt::format(path_cancel_order_, params.settle(), params.order_id());
  auto context = GetAuth()->CreateRequestByDeleteWithSignature(path, &builder);
  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioFuturesNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe)
    -> HttpContextPtr {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

auto GateioFuturesNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe)
    -> HttpContextPtr {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

auto GateioFuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  RestParams params;
  std::string contract = order_spec.product;
  params.set_contract(contract);
  boost::to_upper(contract);
  if (boost::ends_with(contract, "USDT")) {
    params.set_settle("usdt");
  } else if (boost::ends_with(contract, "USD")) {
    params.set_settle("btc");
  } else {
    NOTREACHED() << "Unknown settle type: " << contract;
  }
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    auto product = GateioFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_size(formatter.FormatQty(order_spec.qty));
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_size(formatter.FormatQty(order_spec.qty * -1));
    } else {
      NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
    }
  } else {
    NOTREACHED() << "Unsupported order type: " << static_cast<int>(order_spec.order_type);
  }
  if (order_spec.duration == coin::proto::OrderDuration::GTC_ORDER) {
    params.set_time_in_force("gtc");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("ioc");
  } else {
    NOTREACHED() << "Unsupported order type: " << static_cast<int>(order_spec.order_type);
    params.set_time_in_force("gtc");
  }
  params.set_text(fmt::format("t-{}", proc_order_id));

  return params;
}

auto GateioFuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  RestParams params;
  params.set_order_id(order_context.exchange_order_id());
  std::string contract = order_context.order_spec().product;
  boost::to_upper(contract);
  if (boost::ends_with(contract, "USDT")) {
    params.set_settle("usdt");
  } else if (boost::ends_with(contract, "USD")) {
    params.set_settle("btc");
  } else {
    NOTREACHED() << "Unknown settle type: " << contract;
  }

  return params;
}

std::shared_ptr<AsioHttpContext> GateioFuturesNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> GateioFuturesNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      RestParams params;
      params.set_settle("usdt");
      return QueryAccountBalance(params);
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      RestParams params;
      params.set_settle("usdt");
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
